package nr.co.mhgames.polyanim.util;

import java.awt.Color;
import java.io.File;
import java.util.StringTokenizer;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import nr.co.mhgames.polyanim.Animation;
import nr.co.mhgames.polyanim.Group;
import nr.co.mhgames.polyanim.KeyFrame;
import nr.co.mhgames.polyanim.Polygon;
import nr.co.mhgames.polyanim.TimedKeyFrame;
import nr.co.mhgames.polyanim.loader.XMLAnimationLoader;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 * Simple and unsupported SVG to PolyAnim XML converter. This utility takes a
 * specifically made <a href="www.inkscape.org">Inkscape</a> generated SVG file,
 * parses it in, and saves the results back to a PolyAnim compatible XML file.
 * The resulting XML file can then be edited further and loaded as an animation.
 * 
 * @author Mika Halttunen
 */
public class ConvertSVG {
	private static float globalOriginX = 0;
	private static float globalOriginY = 0;
	private static float globalScale = 1.0f;

	public static Animation loadFromSVG(File file) throws Exception {
		Animation anim = null;
		// Parse the SVG file in
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		DocumentBuilder builder = factory.newDocumentBuilder();

		Document doc = builder.parse(file);

		// Check the root node
		Element rootElem = doc.getDocumentElement();
		String root = rootElem.getNodeName();
		if ("svg".equals(root) == false) {
			throw new Exception("The input file " + file.getName() + " is not a SVG file!");
		}

		// Start building the animation
		anim = new Animation(file.getName());

		// Load the Inkscape layers as key frames
		for (Node child = rootElem.getFirstChild(); child != null; child = child.getNextSibling()) {
			if (child.getNodeType() == Node.ELEMENT_NODE && child.getNodeName().equals("g")) {
				Element frameElem = (Element) child;
				// Ignore layers that start with '-'
				String label = frameElem.getAttribute("inkscape:label");
				if (label.startsWith("-")) {
					System.out.println("\n [INFO] Ignoring layer \"" + label + "\"..");
					continue;
				}

				// Parse the key frame
				TimedKeyFrame newFrame = parseKeyFrame(frameElem);
				anim.addKeyFrame(newFrame);
			}
		}

		// Apply the global origin offset and scale
		for (TimedKeyFrame frame: anim.getKeyFrames()) {
			for (Group g: frame.getFrame().getGroups().values()) {
				Vector2f offset = g.getOffset();
				// offset.x -= globalOriginX;
				// offset.y -= globalOriginY;
				offset.x *= globalScale;
				offset.y *= globalScale;


				for (Polygon p: g.getPolygons()) {
					for (Vertex v: p.getVertices()) {
						v.position.x -= globalOriginX;
						v.position.y -= globalOriginY;
						v.position.x *= globalScale;
						v.position.y *= globalScale;
					}
				}
			}
		}

		return anim;
	}


	private static Vertex getVertex(String str) {
		// Split around the comma
		String parts[] = str.split(",");

		Vertex vert = new Vertex();
		try {
			vert.position.x = Float.parseFloat(parts[0].trim());
			vert.position.y = Float.parseFloat(parts[1].trim());
		} catch (NumberFormatException e) {
			vert.position.x = vert.position.y = 0;
		}

		return vert;
	}

	private static void parseVertices(Polygon poly, String verts) throws Exception {
		// Check the path
		verts = verts.toUpperCase();
		if (verts.charAt(0) != 'M')
			throw new Exception("Invalid path: " + verts + "\nCannot parse the polygon vertices!");

		// Parse the vertices
		StringTokenizer tokenizer = new StringTokenizer(verts, " MLZ");
		while (tokenizer.hasMoreTokens()) {
			String part = tokenizer.nextToken();
			Vertex v = getVertex(part);
			poly.addVertex(v);
		}

		// Remove the last one, as it's a duplicate of the first
		poly.getVertices().remove(poly.getVertexCount() - 1);
	}


	private static float parseValue(String styleString, String key, float defaultValue) {
		float value = defaultValue;

		int index = styleString.indexOf(key);
		if (index >= 0) {
			index += key.length();

			// Parse the value, read up to ; or "
			int end1 = styleString.indexOf(';', index + 1);
			int end2 = styleString.indexOf('"', index + 1);

			// Which is first..
			int end = -1;
			if (end1 < 0 && end2 >= 0)
				end = end2;
			else if (end2 < 0 && end1 >= 0)
				end = end1;
			else
				end = styleString.length() - 1;

			// Try to parse
			if (end > 0) {
				try {
					value = Float.parseFloat(styleString.substring(index, end));
				} catch (NumberFormatException e) {
					return defaultValue;
				}
			}
		}

		return value;
	}


	private static Polygon parsePolygon(Element polyElem) throws Exception {
		Polygon poly = new Polygon();

		// Parse the color
		String style = polyElem.getAttribute("style");
		int index = style.indexOf("fill:");
		if (index < 0)
			poly.setColor(Color.magenta);
		else {
			// #rrggbb
			index += "fill:".length();
			String hexColor = style.substring(index, index + 7);

			// RGB color, must parse alpha separately
			Color color = Color.decode(hexColor);

			// Parse the opacity / alpha, combining fill-opacity and opacity, if
			// both are set in the SVG
			float fillOpacity = parseValue(style, "fill-opacity:", 1.0f);
			style = style.replace("fill-opacity:", "");
			float opacity = parseValue(style, "opacity:", 1.0f);
			float alpha = fillOpacity * opacity;
			assert (alpha >= 0 && alpha <= 1.0f);

			if (alpha < 1.0f) {
				int a = (int) (255.0f * alpha);
				// RGBA
				color = new Color(color.getRed(), color.getGreen(), color.getBlue(), a);
			}

			poly.setColor(color);
		}


		// Parse the vertices
		String vertices = polyElem.getAttribute("d");
		parseVertices(poly, vertices);

		return poly;
	}


	private static void parseMatrix(Group group, String matrixStr) {
		float values[] = new float[6];

		// Parse the values
		int i = 0;
		StringTokenizer tokenizer = new StringTokenizer(matrixStr, ", ");
		while (tokenizer.hasMoreTokens()) {
			String part = tokenizer.nextToken();

			float value = Float.parseFloat(part);
			values[i++] = value;
		}


		// Translation
		group.setOffset(values[4], values[5]);

		// Scale and rotation have to be decomposed from the matrix, this gets
		// hairy..
		double sx = Math.sqrt(values[0] * values[0] + values[2] * values[2]);
		double sy = Math.sqrt(values[1] * values[1] + values[3] * values[3]);

		double cos1 = values[0] / sx;
		double cos2 = values[3] / sy;
		double sin1 = -values[1] / sx;

		sx = values[0] / cos1;
		sy = values[3] / cos2;

		// Scale
		group.setScale((float) sx, (float) sy);

		// Rotation
		double angleRadians = Math.acos(cos1);
		if (!(angleRadians < 0 || angleRadians > 0))
			angleRadians = 0;
		double angleDeg = Math.toDegrees(angleRadians);

		// Find out in which side we are
		if (sin1 >= 0)
			group.setRotation((float) -angleDeg);
		else
			group.setRotation((float) angleDeg);
	}

	private static Group parseGroup(Element groupElem, int depth) throws Exception {
		// Parse the name
		Group group = new Group();
		group.setName(groupElem.getAttribute("inkscape:label"));

		group.setDepthValue(depth);

		// Parse the transformation
		String trans = groupElem.getAttribute("transform");
		if (trans.startsWith("translate")) {
			// Simple translation
			String value = trans.substring(trans.indexOf('(') + 1, trans.length() - 1);
			String parts[] = value.split(",");
			try {
				float x = Float.parseFloat(parts[0]);
				float y = Float.parseFloat(parts[1]);
				group.setOffset(x, y);
			} catch (NumberFormatException e) {
				group.setOffset(0, 0);
			}
		} else if (trans.startsWith("matrix")) {
			// Matrix transformation
			parseMatrix(group, trans.substring(trans.indexOf('(') + 1, trans.length() - 1));
		}

		// Parse polygons
		for (Node poly = groupElem.getFirstChild(); poly != null; poly = poly.getNextSibling()) {
			if (poly.getNodeType() == Node.ELEMENT_NODE && poly.getNodeName().equals("path")) {
				Element polyElem = (Element) poly;
				Polygon newPoly = parsePolygon(polyElem);

				group.addPolygon(newPoly);
			}
		}


		return group;

	}


	private static TimedKeyFrame parseKeyFrame(Element frameElem) throws Exception {
		TimedKeyFrame frame = new TimedKeyFrame();

		// Get the duration from the label
		String label = frameElem.getAttribute("inkscape:label").toLowerCase();

		// Get rid of s and sec(s)
		label = label.replace("sec", "");
		label = label.replace("s", "");

		// Convert to seconds
		try {
			float duration = Float.parseFloat(label);
			frame.setDuration(duration);
		} catch (NumberFormatException e) {
			frame.setDuration(1.0f);
		}

		KeyFrame keyFrame = new KeyFrame();

		// Parse the groups
		int z = 0;
		for (Node group = frameElem.getFirstChild(); group != null; group = group.getNextSibling()) {
			if (group.getNodeType() == Node.ELEMENT_NODE && group.getNodeName().equals("g")) {
				Element groupElem = (Element) group;
				Group newGroup = parseGroup(groupElem, z++);

				keyFrame.addGroup(newGroup);
			} else if (group.getNodeType() == Node.ELEMENT_NODE
					&& group.getNodeName().equals("rect")) {
				Element rectElem = (Element) group;
				// Origin rectangle; origin is in the middle of it
				if (rectElem.getAttribute("inkscape:label").equalsIgnoreCase("origin")) {
					String xs = rectElem.getAttribute("x");
					String ys = rectElem.getAttribute("y");
					String ws = rectElem.getAttribute("width");
					String hs = rectElem.getAttribute("height");

					float x = Float.parseFloat(xs);
					float y = Float.parseFloat(ys);
					float w = Float.parseFloat(ws);
					float h = Float.parseFloat(hs);

					globalOriginX = x + w / 2;
					globalOriginY = y + h / 2;
					System.out.println(" [INFO] Global origin set to: " + globalOriginX + ","
							+ globalOriginY);
				}
			}
		}

		frame.setFrame(keyFrame);
		return frame;
	}


	/**
	 * Starting point
	 * 
	 * @param args
	 *            The command-line arguments
	 */
	public static void main(String args[]) {
		System.out.println("ConvertSVG - unsupported Inkscape SVG to PolyAnim XML converter.");
		System.out.println("-----------------------------------------------------------------");

		if (args.length < 2) {
			// Print the usage
			System.out.println("Usage:\n" + "java -cp bin nr.co.mhgames.polyanim.util.ConvertSVG"
					+ " [SVG input] [XML output] [[scale]]");
			System.out
					.println("Note: The scale value is optional, it defaults to 1.0 (no scaling).");
			System.out.println("\nExample 1 - (convert dude.svg to dude.xml):\n"
					+ "java -cp bin nr.co.mhgames.polyanim.util.ConvertSVG" + " dude.svg dude.xml");
			System.out.println("\nExample 2 - (convert dude.svg to dude.xml, scale 2x):\n"
					+ "java -cp bin nr.co.mhgames.polyanim.util.ConvertSVG"
					+ " dude.svg dude.xml 2.0");
			System.exit(0);
		}

		try {
			// Get the scale value, if set
			if (args.length > 2) {
				globalScale = Float.parseFloat(args[2]);
				System.out.println(" [INFO] Scale factor set to " + globalScale + ".");
			}

			// Parse the SVG
			System.out.print("Parsing '" + args[0] + "'.. ");
			Animation anim = loadFromSVG(new File(args[0]));
			System.out.println("ok.");

			System.out.println(anim + "\n");


			// Save to XML
			System.out.print("Saving the animation to '" + args[1] + "'.. ");
			XMLAnimationLoader loader = new XMLAnimationLoader();
			loader.writeAnimation(anim, args[1]);
			System.out.println("ok.");

		} catch (Exception e) {
			System.err.println("Error while converting '" + args[0] + "' to '" + args[1] + "':");
			e.printStackTrace(System.err);
		}
	}
}
